'''
Created on 21.9.2011

@author: mphkh
'''
from CashFlowSchedule import *
from CashFlowUtils.CpiCurve import *
from datetime import date
from TimeUtils import *

class BondBase(object):
    '''
    Due to we are doing backward dating, a maturity date nor interest 
    rate day can't be any later in the month than the 28th.
    '''
    description = 'bondbase'
    
    def __init__(self, caldar, issuedate, firstinterestdate, firstcoupondate, maturitydate, daterollingconvention, daycountbasis, frequencyyear=1):
        self.bondcfschedule = CashFlowSchedule(self.caldar, self.issuedate, self.firstinterestdate, self.firstcoupondate, self.maturity, self.daterollingconvention, self.daycountbasis, self.frequencyyear)
        self.bondcfs = self.bondcfschedule.cashflows()
    
    def cashflows(self):
        self.bondcfschedule   = CashFlowSchedule(self.caldar, self.issuedate, self.firstinterestdate, self.firstcoupondate, self.maturitydate, self.daterollingconvention, self.daycountbasis, self.frequencyyear)
        self.bondcfs          = self.bondcfschedule.cashflows()
        return self.bondcfs

    'Cashflow due on settlementdate is omitted.'
    def remaining_cashflows(self, settlementdate=None):
        bondcfs = self.cashflows()
        remainingcfs = []

        if settlementdate is None:
            settlementdate = date.today()
        for item in bondcfs:
            if item.cfdate>settlementdate:
                remainingcfs.append(item)

        nextcf = remainingcfs[0]
        #accruedfctr = yearfraction(settlementdate, nextcf.cfdate, self.daycountbasis)
        accruedfctr = (nextcf.cfdate.toordinal() - settlementdate.toordinal()) / nextcf.daysPeriod

        i = 0
        for item in remainingcfs:
            remainingcfs[i].yearfrac = accruedfctr + i/self.frequencyyear
            i = i+1
        return remainingcfs

    'Cashflow due on settlementdate is omitted but cashflow on todate is included.'
    def remaining_cashflows_todate(self, settlementdate, todate):
        # Note: We return cashflows on the given dates included.
        bondcfs = self.cashflows()
        remainingcfs = []
        for item in bondcfs:
            if item.cfdate>=settlementdate and item.cfdate<=todate:
                remainingcfs.append(item)
        
        if len(remainingcfs)>0:
            nextcf = remainingcfs[0]
            #accruedfctr = yearfraction(settlementdate, nextcf.cfdate, self.daycountbasis)
            accruedfctr = (nextcf.cfdate.toordinal() - settlementdate.toordinal()) / nextcf.daysPeriod
        else:
            accruedfctr = 0.0

        i = 0
        for item in remainingcfs:
            remainingcfs[i].yearfrac = accruedfctr + i/self.frequencyyear
            i = i+1     
        return remainingcfs
    
    def next_cashflow(self, settlementdate):
        remainingcfs = self.remaining_cashflows(settlementdate)
        #accruedfctr = yearfraction(settlementdate, remainingcfs[0].cfdate, self.daycountbasis)
        accruedfctr = (remainingcfs[0].cfdate.toordinal() - settlementdate.toordinal()) / remainingcfs[0].daysPeriod

        remainingcfs[0].yearfrac = accruedfctr
        return remainingcfs[0]
    
    def previous_cashflow(self, settlementdate):
        bondcfs = self.cashflows()
        current_cf = bondcfs[0]
        for item in bondcfs:
            if item.cfdate>settlementdate:
                return current_cf
            else:
                current_cf = item
        return current_cf   
    
    def duration(self, settlementdate, yld):
        remainingcfs = self.remaining_cashflows(settlementdate)
        cfsumdf = 0.0
        cfsumdfWeighted = 0.0
        i = 0
        for item in remainingcfs:
            cfPV = item.amount * (1+yld)**-item.yearfrac
            cfsumdf = cfsumdf + cfPV
            cfsumdfWeighted = cfsumdfWeighted + cfPV*item.yearfrac
            i = i + 1
        return cfsumdfWeighted / cfsumdf
    
    def yld(self, settlementdate, price, ACCURACY=0.0001, MAXITERATIONS=1000):
        bot = 0.0
        top = 1.0
        trgyield = 0.5*(top+bot)
        for i in range(MAXITERATIONS):
            tempprice = self.cleanprice(settlementdate, trgyield)
            diff = tempprice - price
            if abs(diff)<ACCURACY:
                return trgyield
            if diff>0.0:
                bot = trgyield
            else:
                top = trgyield
            trgyield = 0.5*(top+bot)
        return trgyield


class FixedRateBond(BondBase):
    def __init__(self, calendar, issuedate, firstinterestdate, firstcoupondate, maturitydate, daycountbasis, daterollingconvention, 
                 faceamount, couponrate, frequencyyear=1):
        self.calendar               = calendar
        self.issuedate              = issuedate
        self.firstinterestdate      = firstinterestdate
        self.firstcoupondate        = firstcoupondate
        self.maturitydate           = maturitydate
        self.daycountbasis          = daycountbasis
        self.daterollingconvention  = daterollingconvention
        self.faceamount             = faceamount
        self.couponrate             = couponrate
        self.frequencyyear          = frequencyyear

    def cashflows(self):
        self.bondcfschedule   = CashFlowSchedule(self.calendar, self.issuedate, self.firstinterestdate, self.firstcoupondate, self.maturitydate, self.daterollingconvention, self.daycountbasis, self.frequencyyear)
        self.bondcfs          = self.bondcfschedule.cashflows()
        for item in self.bondcfs:
            item.interests = self.faceamount*self.couponrate/self.frequencyyear
            item.amount   = self.faceamount*self.couponrate/self.frequencyyear
        self.bondcfs[-1].amount = self.bondcfs[-1].amount + self.faceamount
        self.bondcfs[-1].notional = self.faceamount
        return self.bondcfs

    def dirtyprice(self, settlementdate, yld):
        remainingcfs = self.remaining_cashflows(settlementdate)
        cfsumdf = 0.0
        i = 0
        for item in remainingcfs:
            cfPV = item.amount * (1+yld)**-item.yearfrac
            cfsumdf = cfsumdf + cfPV
            i = i + 1
        return cfsumdf
    
    def cleanprice(self, settlementdate, yld):
        remainingcfs = self.remaining_cashflows(settlementdate)
        cfsumdf = 0.0
        i = 0
        for item in remainingcfs:
            cfPV = item.amount * (1+yld)**-item.yearfrac 
            cfsumdf = cfsumdf + cfPV 
            i = i + 1
        ncf = self.next_cashflow(settlementdate)
        accrued = ncf.interests * (1 - remainingcfs[0].yearfrac)
        return cfsumdf - accrued

    def cleantodirty(self, settlementdate, price):
        remainingcfs = self.remaining_cashflows(settlementdate)
        ncf = self.next_cashflow(settlementdate)
        accrued = ncf.interests * (1- remainingcfs[0].yearfrac)
        return price + accrued

class IndexedFixedRateBond(FixedRateBond):
    def __init__(self, calendar, issuedate, firstinterestdate, firstcoupondate, maturitydate, daycountbasis, daterollingconvention, 
                 faceamount, couponrate, frequencyyear=1):
        self.calendar               = calendar
        self.issuedate              = issuedate
        self.firstinterestdate      = firstinterestdate
        self.firstcoupondate        = firstcoupondate
        self.maturitydate           = maturitydate
        self.daycountbasis          = daycountbasis
        self.daterollingconvention  = daterollingconvention
        self.faceamount             = faceamount
        self.couponrate             = couponrate
        self.frequencyyear          = frequencyyear
        self.cpibase                = 0.0

    def cashflows(self):
        self.bondcfschedule   = CashFlowSchedule(self.calendar, self.issuedate, self.firstinterestdate, self.firstcoupondate, self.maturitydate, self.daterollingconvention, self.daycountbasis, self.frequencyyear)
        self.bondcfs          = self.bondcfschedule.cashflows()
        for item in self.bondcfs:
            item.interest = self.couponrate/self.frequencyyear
            item.amount   = self.faceamount*self.couponrate/self.frequencyyear
        self.bondcfs[-1].amount = self.bondcfs[-1].amount + self.faceamount
        return self.bondcfs

    def dirtyprice(self, settlementdate, yld):
        a = CpiCurve()
        dcpi = a.dailycpi(settlementdate)
        
        remainingcfs = self.remaining_cashflows(settlementdate)
        cfsumdf = 0.0
        
        i = 0
        ncf = self.next_cashflow(settlementdate)
        yfn = yearfraction(settlementdate, ncf.cfdate, self.daycountbasis)
        for item in remainingcfs:
            cfpv = item.amount * (1+yld)**-(yfn+i) 
            cfsumdf = cfsumdf + cfpv 
            i = i + 1
        return cfsumdf*dcpi/self.cpibase

    def cleanprice(self, settlementdate, yld):
        remainingcfs = self.remaining_cashflows(settlementdate)
        cfsumdf = 0.0
        i = 0
        for item in remainingcfs:
            cfPV = item.amount * (1+yld)**-item.yearfrac 
            cfsumdf = cfsumdf + cfPV 
            i = i + 1
        ncf = self.next_cashflow(settlementdate)
        accrued = ncf.amount * (1 - remainingcfs[0].yearfrac)
        return cfsumdf - accrued
    
    def cleantodirty(self, settlementdate, price):
        a = CpiCurve()
        dcpi = a.dailycpi(settlementdate)

        remainingcfs = self.remaining_cashflows(settlementdate)
        ncf = self.next_cashflow(settlementdate)
        accrued = ncf.amount * (1- remainingcfs[0].yearfrac)

        dirtyundadjusted = price + accrued

        return dirtyundadjusted*dcpi/self.cpibase

        return price + accrued
    
class HFFBond(BondBase):
    '''
    Implementation of Icelandic inflation-linked annuity bonds issued by the Housing Finance Fund in Iceland.
    '''
    def __init__(self, calendar, issuedate, firstinterestdate, firstcoupondate, maturitydate, daycountbasis, daterollingconvention, 
                 faceamount, couponrate, frequencyyear=1):
        self.calendar               = calendar
        self.issuedate              = issuedate
        self.firstinterestdate      = firstinterestdate
        self.firstcoupondate        = firstcoupondate
        self.maturitydate           = maturitydate
        self.daycountbasis          = daycountbasis
        self.daterollingconvention  = daterollingconvention
        self.faceamount             = faceamount
        self.couponrate             = couponrate
        self.frequencyyear          = frequencyyear
        self.bondcfschedule         = CashFlowSchedule(self.calendar, self.issuedate, self.firstinterestdate, self.firstcoupondate, self.maturitydate, self.daterollingconvention, self.frequencyyear)
        self.bondcfs                = self.bondcfschedule.cashflows()
        self.cpibase                = 0.0

    def cashflows(self):
        FACEVALUE = 1000000.0
        r                     = self.couponrate/self.frequencyyear
        self.bondcfschedule   = CashFlowSchedule(self.calendar, self.issuedate, self.firstinterestdate, self.firstcoupondate, self.maturitydate, self.daterollingconvention, self.daycountbasis, self.frequencyyear)
        self.bondcfs          = self.bondcfschedule.cashflows()

        numberofcfs = len(self.bondcfs)
        
        i = 0
        for item in self.bondcfs:
            'Remaining notional is assigned to amount'
            remainingnotional = FACEVALUE*((1+r)**i - ((1+r)**i-1) / (1-(1+r)**(-numberofcfs)))
            #annuitypayment = FACEVALUE*(r)/(1-(1+r)**(-numberofcfs))
            item.amortization = FACEVALUE*r*(1+r)**(i-1)/((1+r)**(numberofcfs)-1)

            'Interests amount payable'
            item.interests = FACEVALUE*r*((1+r)**numberofcfs - (1+r)**(i-1)) / ((1+r)**numberofcfs - 1)
            item.amount = item.amortization + item.interests
            'We dont have any cashflow movements on initiation day of the bond.'
            if i == 0:
                item.notional = FACEVALUE
                item.amortization = 0.0
                item.amount = 0.0
            else:
                item.notional = remainingnotional + item.amortization
            i = i+1
        self.bondcfs[0].interests = 0.0
        return self.bondcfs

    def dirtyprice(self, settlementdate, yld):
        FACEVALUE = 1000000.0
        a = CpiCurve()
        dcpi = a.dailycpi(settlementdate)
        r = self.couponrate/self.frequencyyear
        numberofcfs = len(self.cashflows())
        
        'The annuity payment P to amortise a notional FACEVALUE'
        annuitypayment = FACEVALUE*(r)/(1-(1+r)**(-numberofcfs))
        
        remainingcfs = self.remaining_cashflows(settlementdate)
        cfsumdf = 0.0
        for item in remainingcfs:
            daysAccrued = daysbetween(settlementdate, item.cfdate, self.daycountbasis)
            cfsumdf = cfsumdf + (1+yld)**(-daysAccrued/360.0)
        pv = annuitypayment*cfsumdf
        
        'PV adjusted with inflation results in dirty price'
        dirtyprice = pv/FACEVALUE*dcpi/self.cpibase*100.0
        
        return dirtyprice

    def cleanprice(self, settlementdate, yld):
        FACEVALUE = 1000000.0
        r = self.couponrate/self.frequencyyear
        numberofcfs = len(self.cashflows())

        'The annuity payment P to amortise a notional FACEVALUE'
        annuitypayment = FACEVALUE*(r)/(1-(1+r)**(-numberofcfs))
        
        remainingcfs = self.remaining_cashflows(settlementdate)
        cfsumdf = 0.0
        for item in remainingcfs:
            daysAccrued = daysbetween(settlementdate, item.cfdate, self.daycountbasis)
            cfsumdf = cfsumdf + (1+yld)**(-daysAccrued/360.0)
        pv = annuitypayment*cfsumdf
        
        daysAccrued = daysbetween(settlementdate, remainingcfs[0].cfdate, self.daycountbasis)

        'We work out remaining notional after next amortization payment'
        remainingnotionalnxtcf =  remainingcfs[0].notional - remainingcfs[0].amortization
        
        'Accrued interests'
        accruedinterests = r*remainingnotionalnxtcf*(1-daysAccrued/180.0)
        
        'Clean price unadjusted for remaining notional'
        cleanpriceunadj = pv-accruedinterests

        'Clean price, we adjust the clean price to notional of 100'
        cleanprice = 100.0*cleanpriceunadj/remainingnotionalnxtcf  
        
        return cleanprice
        
    def cleantodirty(self, settlementdate, price):
        FACEVALUE = 1000000.0
        a = CpiCurve()
        dcpi = a.dailycpi(settlementdate)
        r = self.couponrate/self.frequencyyear

        remainingcfs = self.remaining_cashflows(settlementdate)
        daysAccrued = daysbetween(settlementdate, remainingcfs[0].cfdate, self.daycountbasis)

        'We work out remaining notional after next amortization payment'
        remainingnotionalnxtcf =  remainingcfs[0].notional - remainingcfs[0].amortization

        'Accrued interests'
        accruedinterests = r*remainingnotionalnxtcf*(1-daysAccrued/180.0)

        'Clean price unadjusted for remaining notional'
        cleanpriceunadj = price*remainingnotionalnxtcf/100.0

        pv = cleanpriceunadj + accruedinterests

        dirtyprice = pv/FACEVALUE*dcpi/self.cpibase*100.0

        return dirtyprice

